fix(rules): prove ContainerCpuThrottled can fire, and say that it cannot (#185) - #318
Merged
Conversation
…not (#185) The rule has never been able to fire. CFS accounting only exists when a CPU quota exists, no service sets one, so both counters sit at 0 forever and the numerator can never be non-zero. `promtool check rules` passes it — it parses PromQL and never asks whether an expression can be true — and it shows loaded and healthy on the status page. That is #63 exactly, one rule up in the same file. Not fixed by adding `cpus:`, for the reasons #185 sets out: the stack peaks at ~0.4 of 4 cores, HostHighLoad already covers saturation at load15 > 2 x cores, and a quota tight enough to matter would be saturated inside individual 100ms CFS windows during a scrape burst — manufacturing alert fatigue to justify a control that prevents nothing. So it is kept and made honest instead. The rule now says it is inert, why a quota is the wrong answer, and exactly what would change that: any service gaining `cpus:` or `deploy.resources.limits.cpus`. The tests are what make that safe. A firing case against synthetic CFS series — 600 periods a minute at the default 100ms period, 180 throttled, 30% against a 0.25 threshold — and a quiet case with both counters flat at 0, which is what cAdvisor actually reports here. Mutation-tested rather than trusted: adding `and container_spec_cpu_quota > 0` (the #63 shape) and raising the threshold past the fixture both fail the suite. `rate()` returns a value from the second sample rather than waiting for its 15m window, so the alert fires at 31m and not 45m. promtool caught that; the paired 25m case now asserts the `for:` rather than assuming it. observability.md's coverage counts moved with the new test, 27 to 28 tested and 29 to 28 not. check_docs.py caught that. The same paragraph still told authors to keep each count on one line because the checker read prose line by line — untrue since #209 made it whole-file — so that advice is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #185, taking the path the issue argues for rather than the one that would make the rule live.
Not adding a CPU quota
Agreed, and the reasoning is now in the rule itself rather than only in the issue: the stack peaks at ~0.4 of 4 cores,
HostHighLoadalready fires atload15 > 2 x cores, and a quota tight enough to matter would be saturated inside individual 100ms CFS windows during a scrape burst — manufacturing alert fatigue to justify a control that prevents nothing.The rule says what it is
A comment stating it is inert, why a quota is the wrong answer, and exactly what would make it live: any service gaining
cpus:ordeploy.resources.limits.cpus. Nothing else. It is kept against the day a quota is set for some other reason — a noisy-neighbour guest, a service that genuinely needs bounding.The tests are what make keeping it safe
Per the convention in
containers.test.yaml's header, a firing case paired with a quiet one:clamp_minholds the denominator at 1, so this evaluates to 0 rather than NaN.Mutation-tested rather than trusted. A green suite proves nothing on its own — that is the #63 lesson. Both of these fail it:
and container_spec_cpu_quota > 0appended — the exact #63 shape, a guard on a series production reports as 0> 0.95, above the fixtureOne correction from writing them: I had the
for:arithmetic wrong, assumingrate()waits for its 15m window to fill. It returns a value from the second sample, so the alert is pending at 1m and fires at 31m, not 45m. promtool caught it, and the paired 25m case now asserts thefor:rather than assuming it.Two knock-ons
docs/observability.md's coverage counts moved — 27→28 tested, 29→28 not.check_docs.pycaught it, which is the docs/observability.md says three receivers; there are four #212/Five places say seven VLANs; the firewall has six, and this repo's own tables already list six #209 machinery working on a change unrelated to either.Checks
validate.sh— 48 PASS, 0 FAIL, 2 SKIP.promtool test rulesnow covers 9 files in observability, up from 8.🤖 Generated with Claude Code